﻿//
// © Algobox Ltd - Benn Eichhorn
//
using Algobox.Objects.Finance.Assets.Stock;
using Algobox.Objects.Finance.Orders;
using System.Collections.Generic;
using System.Linq;
using System;

namespace Algobox.Structure.Finance.Strategies.MarketFiller
{
    public class MarketFillOrder : Algobox.Feeds.Finance.Trading.IOrderEvent
    {
        #region Constructors

        /// <summary>
        /// Attempts to get the best price achieveable accross listing exchanges to fill a market order
        /// </summary>
        /// <param name="tradingFeed"></param>
        /// <param name="callback"></param>
        /// <param name="security"></param>
        /// <param name="account"></param>
        /// <param name="isBuy"></param>
        /// <param name="priceTarget"></param>
        /// <param name="priceStop"></param>
        /// <param name="quantity"></param>
        public MarketFillOrder(
            Algobox.Feeds.Finance.Trading.ITradingFeedAdaptor tradingFeed, 
            IMarketFillerCallback callback, 
            StockSecurity security, 
            string account, 
            bool isBuy,
            double startPrice,
            uint quantity,
            Order.TimeInForce orderTimeInForce)
        {
            _orderId = System.Threading.Interlocked.Increment(ref _orderIdCounter) + "|" + callback.Id + "|" + security.ToString();
            _priceStart = (decimal)startPrice;
            _tradingFeed = tradingFeed;
            _callback = callback;
            _security = security;
            _isBuy = isBuy;
            _quantityRequired = quantity;
            _quantityRemaining = quantity;
            _account = account;
            _orderTimeInForce = orderTimeInForce;

            _listings = new List<StockListing>(security.Listings.Values.OrderBy(a => a.Priority));

            System.Diagnostics.Debug.Assert(_listings.Count > 0, "No listings for this listing???");

            if(Log.IsInfoEnabled)
                Log.Info("{0}|Creating market fill order {1} {2} for {3} in {4} limit mode starting price at {5}", _orderId, isBuy ? "buying" : "selling", quantity, callback.Id, orderTimeInForce, startPrice);
        }

        #endregion
//////////////////
        #region Member Variables

        /// <summary>
        /// Maximum number of order failures permitted
        /// </summary>
        private const int MAX_RETRIES = 6;

        private static long _orderIdCounter = 0;
        private readonly string _orderId;
    
        private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger();

        private Algobox.Feeds.Finance.Trading.ITradingFeedAdaptor _tradingFeed;

        private readonly object _lock = new object();

        private readonly IMarketFillerCallback _callback;
        private readonly StockSecurity _security;
        private readonly string _account;
        private readonly bool _isBuy;
        private readonly uint _quantityRequired;
        private readonly Order.TimeInForce _orderTimeInForce;

        private bool _isStarted;
        private uint _quantityRemaining;
        private double _averagePrice;

        private int _retiresCount;

        private List<StockListing> _listings;
        private int _listingIndex;
        private int _indexRestartCount;
        private decimal _priceStart;

        private Order _order;
        private bool _isCancelPending;

        #endregion
//////////////////
        #region Properties

        public uint Quantity { get { return _quantityRequired; } }
        public uint QuantityFilled { get { return _quantityRequired - _quantityRemaining; } }
        public double AveragePrice { get { return _averagePrice; } }
        public DateTime TimeStamp { get; private set; }

        #endregion
//////////////////
        #region Functions

        public void Start()
        {
            if (!_isStarted)
            {
                GoFill();
                _isStarted = true;
            }
        }

        private void GoFill()
        {
            lock (_lock)
            {
                string message;
                while (_quantityRemaining > 0)
                {
                    StockListing listing;

                    if (_listingIndex == _listings.Count)
                    {
                        _indexRestartCount++;
                        _listingIndex = 0;

                        if (Log.IsErrorEnabled)
                        {
                            Log.Error("{0}|GoFill Increasing tick multiplier to {1}", _orderId, _indexRestartCount);
                        }                     
                    }

                    if (_priceStart == 0)
                    {
                        // lets get the consolidated security price
                        listing = _listings[0];
                        _priceStart = _isBuy ? listing.TickSize.Round(listing.Security.Prices.Ask.Price) : listing.TickSize.Round(listing.Security.Prices.Bid.Price);

                        if (_priceStart == 0)
                        {
                            // couldn't get the consolidated security price so go for the listings
                            if (Log.IsWarnEnabled)
                            {
                                Log.Warn("{0}|GoFill security price is 0, trying for listing price", _orderId);
                            }

                            // cycle through each listing until we get a price
                            for (int i = 0; i < _listings.Count; i++)
                            {
                                listing = _listings[i];

                                _priceStart = _isBuy ? listing.TickSize.Round(listing.Prices.Ask.Price) : listing.TickSize.Round(listing.Prices.Bid.Price);

                                if (_priceStart != 0)
                                {
                                    break;
                                }
                                else if (Log.IsErrorEnabled)
                                {
                                    Log.Error("{0}|GoFill listing price is also 0 on {1}", _orderId, listing.Exchange);
                                }
                            }

                            // still don't have a price
                            if (_priceStart == 0)
                            {                                
                                if (Log.IsErrorEnabled)
                                    Log.Error("{0}|GoFill Couldn't get a valid price to execute the order, failing market fill order", _orderId, listing.Exchange);

                                // FAIL HERE
                                _callback.OnMarketFillerFailed(this);
                                return;                                
                            }
                        }
                    }

                    // get the next listing
                    listing = _listings[_listingIndex++];

                    decimal price;

                    // add the tick multiplier to the price
                    if (_isBuy)
                    {
                        price = _priceStart + (listing.TickSize.GetTickSize(_priceStart) * _indexRestartCount);
                    }
                    else
                    {
                        price = _priceStart - (listing.TickSize.GetTickSize(_priceStart) * _indexRestartCount);
                    }

                    // round the price to the nearest tick (probably not necessary, but just in case)
                    price = listing.TickSize.Round(price);                    
                    
                    if (Log.IsTraceEnabled)
                        Log.Trace("{0}|GoFill Attempting to fill order of {1}{2}@{3}", _orderId, _isBuy ? "" : "-", _quantityRemaining, price);

                    _order = new Order(_account, listing, _isBuy, price, _quantityRemaining, Order.OrderType.Limit, _orderTimeInForce);

                    if (_tradingFeed.TrySubmitOrder(_order, this, out message))
                    {
                        if(Log.IsTraceEnabled)
                            Log.Trace("{0}|GoFill TrySubmitOrder success for Order({1}) Message({2})", _orderId, _order.ToString(), message);
                        break;
                    }
                    else
                    {
                        if (++_retiresCount >= MAX_RETRIES)
                        {
                            if (Log.IsErrorEnabled)
                                Log.Error("{0}|GoFill TrySubmitOrder rejected, failing market fill order due to to mainy retries Order({1}) Message({2})", _orderId, _order.ToString(), message);    
                            // FAIL HERE
                            _callback.OnMarketFillerFailed(this);
                            return;
                        }
                        else
                        {
                            if(Log.IsErrorEnabled)
                                Log.Error("{0}|GoFill TrySubmitOrder rejected for Order({1}) Message({2})", _orderId, _order.ToString(), message);                            
                        }
                    }

                    if (_quantityRemaining < 0)
                    {
                        if (Log.IsFatalEnabled)
                            Log.Fatal("{0}|GoFill QuantityRemaining out of range less than zero", _orderId, _order.ToString(), message);      
                    }

                } // while
            } // lock
        }


        private void RegisterFill(uint fillQuantity, double fillAvgPrice)
        {
            if (Log.IsDebugEnabled)
                Log.Debug("{0}|RegisterFill registering fill of {1}@{2}", _orderId, fillQuantity, fillAvgPrice);

            // is this our first fill?
            if (_averagePrice == 0)
            {
                _quantityRemaining -= fillQuantity;
                _averagePrice = fillAvgPrice;
            }
            else
            {
                uint filled = _quantityRequired - _quantityRemaining;

                _averagePrice = ((filled * _averagePrice) + (fillQuantity * fillAvgPrice)) / (filled + fillQuantity);

                _quantityRemaining -= fillQuantity;

                if(Log.IsTraceEnabled)
                    Log.Trace("{0}|RegisterFill Set quantity remaining to {1}", _orderId, _quantityRemaining);
            }
        }


        public void OrderUpdate(Order order, Order.Status state)
        {
            if (Log.IsDebugEnabled)
                Log.Debug("{0}|OrderUpdate recieved order in state {1} Order({1})", _orderId, state, order.ToString());

            lock (_lock)
            {
                string message;
                switch (state)
                {
                    // order complete
                    case Order.Status.Filled:
                    case Order.Status.Canceled:
                    case Order.Status.Stopped:
                        TimeStamp = order.TimeUpdated;
                        if (order.QuantityFilled > 0)
                        {
                            RegisterFill(order.QuantityFilled, (double)order.PriceAverage);                            
                        }
                        _order = null;
                        if (_quantityRemaining == 0)
                        {
                            Log.Debug("{0}|OrderUpdate MarketFiller complete", _orderId);
                            _callback.OnMarketFillerCompleted(this);
                        }
                        else
                        {
                            Log.Debug("{0}|OrderUpdate MarketFiller requires another {1}", _orderId, _quantityRemaining);
                            _isCancelPending = false;
                            GoFill();
                        }
                        break;

                    // deal with a reject
                    case Order.Status.Rejected:
                        if (_quantityRemaining != 0)
                        {
                            if (++_retiresCount >= MAX_RETRIES && _quantityRemaining != 0)
                            {
                                Log.Error("{0}|OrderUpdate Order rejected, failing due to too many retries Order({1}) Text({2})", _orderId, order.ToString(), order.Text);
                                _callback.OnMarketFillerFailed(this);
                                return;
                            }
                            else
                            {
                                if (order.QuantityFilled > 0)
                                {
                                    Log.Error("{0}|OrderUpdate Order rejected, but has a fill of {1}, check previous log entries for Order({2}) Text({3})", _orderId, order.QuantityFilled, order.ToString(), order.Text);
                                }
                                else
                                {
                                    Log.Error("{0}|OrderUpdate Order rejected for Order({1}) Text({2})", _orderId, order.ToString(), order.Text);
                                }
                            }

                            // only restart if we have not been trying to cancel
                            if (!order.Text.Contains("TooLateToCancel"))
                            {
                                Log.Debug("{0}|OrderUpdate MarketFiller requires another {1}", _orderId, _quantityRemaining);
                                _order = null;
                                GoFill();
                            }
                        }
                        else
                        {
                            Log.Error("{0}|OrderUpdate Received a rejected for completed Order({1}) Text({2})", _orderId, order.ToString(), order.Text);
                        }
                        break;

                    case Order.Status.FilledPartial:
                    case Order.Status.New:
                    case Order.Status.Updated:
                        if (!_isCancelPending)
                        {           
                            if (_tradingFeed.TryCancelOrder(_order, out message))
                            {
                                Log.Debug("{0}|OrderUpdate Sent an order cancel", _orderId, state.ToString(), _order.ToString(), order.Text);
                                _isCancelPending = true;
                            }
                            else
                            {
                                // need to do handle this if this ever happens!
                                if (++_retiresCount >= MAX_RETRIES)
                                {
                                    Log.Error("{0}|OrderUpdate Failed to cancel order in state {1}, failing due to too many retries Order({2}) Text({3})", _orderId, state.ToString(), _order.ToString(), order.Text);
                                    // FAIL HERE
                                    _callback.OnMarketFillerFailed(this);
                                    return;
                                }
                                else
                                {
                                    Log.Error("{0}|OrderUpdate Failed to cancel order in state {1} Order({2}) Text({3})", _orderId, state.ToString(), _order.ToString(), order.Text);
                                }
                            }
                        }
                        break;

                    // otherwise wait for the correct state
                    default:
                        break;
                }

            } // end lock
        }

        #endregion

    }
}
